React / Redux Best Practices
Here are three tips each on React and Redux that will help build more idiomatic, maintainable codebases.
Summary
React:
- Minimize use of Component State
- Leverage Latest Javascript Features
- Calculate Everything In render()
Redux:
React
1) Minimize Use of Component State
Especially when used in tandem with a state management library like Redux, React components have very little use for internal state. That being said, knowing when to use internal state and when to lift something up as props is one of the more difficult and arguably subjective aspects of React development. Here are a few things to think about:
- Is this data presentational? In other words, does it affect anything other than the way a component looks?
- Does this data belong solely to this component? If other components rely on the data, or if other components will need to manipulate it in any way, it probably doesn't belong in local state.
- When in doubt, lift data upward! Leaving a datum in internal component state makes it impossible to access at a higher level; lifting it up makes it easy to control from other parts of your application.
- If a component does not own a datum, then that datum should not influence its state.
Consider the example of a modal window with multiple tabs. It is perfectly acceptable and idiomatic to store the name of the active tab in state:
constructor() {
super();
this.state = {
selectedTab: 'All'
};
}
To update the selected tab, each tab might have a click listener with logic like this:
handleClick = tabName => this.setState({ selectedTab: tabName });
This is a fine setup for now, but later we get a new feature request from users. The modal needs to open to a specific tab depending on where the user clicked. We decide to pass down the selected tab as a prop, so it can be accessed by the modal internally. Now our constructor looks like this:
constructor(props) {
super(props);
this.state = {
selectedTab: props.selectedTab || 'All'
};
}
Now the setState click listener will still work for changing tabs, but we had better hope that the component is never re-rendered, or it will go back to rendering based on the selectedTab prop. Now we need to leverage an additional lifecycle method, componentWillReceiveProps
, in order to update the component's internal state.
componentWillReceiveProps(newProps) {
const { selectedTab } = newProps;
if (selectedTab) {
this.setState({ selectedTab });
}
}
Our modal component is getting pretty complicated, all in service of what seems like fairly typical modal behavior. This has all happened because we have effectively synchronized the state between a child component and its parent. Don't do this. We solve this problem by removing state from the component entirely, and storing the selected tab in the parent component. Now our modal component looks like this:
const Modal = ({ selectedTab, updateTab }) =>
<div className="modal-body">
<div className="tabs">
<button className="tab" onClick={updateTab}>
All
</button>
...
</div>
<div className="tab-content">...</div>
</div>;
The entire component is just a function. The currently selected tab is passed down as a prop, and the updateTab prop is a function that updates the parent component's state. Now the modal's appearance can be updated simply by passing down a new prop.
2) Leverage Latest Javascript Features
Next-gen JavaScript features make React development a breeze. Here are a few before-and-after snippets that show the power of ESNext / Babel compilation.
Generating An Array of Child Components
The old way (for loop, method binding in constructor):
constructor() {
super();
this.renderTodoList.bind(this);
}
renderTodoList(arrayOfTodos) {
var arrayOfComponents = [];
for (i = 0; i < arrayOfTodos.length; i++) {
var thisTodo = arrayOfTodos[i];
arrayOfComponents.push(<Todo key={thisTodo.id} text={thisTodo.text} />);
}
return arrayOfComponents;
}
The new way (ES6 arrows, inherent binding, map utility)
renderTodoList = arrayOfTodos =>
arrayOfTodos.map(todo => <Todo key={todo.id} {...todo} />);
Pass Down Props To Child
Old way (constantly reference this.props
)
render() {
return (
<TodoList
type={this.props.todoType}
todos={this.props.todos}
visible={!this.props.isHidden}
/>
);
}
New way (more DRY)
render() {
const { todoType, todos, isHidden } = this.props;
return <TodoList type={todoType} todos={todos} visible={!isHidden} />;
}
In fact, with a little manipulation of prop names it could even be:
const Todo = props => <TodoList {...props} />
Dynamic CSS className
s (String Concatenation)
Old way (hard to read)
const myClassyComponent = <div className={'modal ' + (this.props.hidden ? 'hidden' : '') + (this.props.disabled ? 'disabled' : '')} />
New way (very clear)
const { hidden, disabled } = this.props;
const hiddenClass = hidden ? 'hidden' : '';
const disabledClass = disabled ? 'disabled' : '';
const myClassyComponent = <div className={`modal ${hiddenClass} ${disabledClass}`} />;
Template strings also come in handy for networking code:
const apiFetch = (path, hostName = 'http://localhost:3001') =>
fetch(`${hostName}/${path}`).then(res => res.json());
3) Calculate Everything In render()
If you follow the guidance above in regard to component state, you shouldn't run into this issue, but it's still worth a mention. Do not store derived data in component state! Any calculations or conditionals should be evaluated at the last minute, in the component's render function. A common mistake for less-experienced React devs is to do something like this:
EXAMPLE OF WHAT NOT TO DO
constructor(props) {
const itemTotal = this.props.items.reduce(
(sum, thisItem) => sum + thisItem.total,
0
);
this.state = { itemTotal };
}
render() {
return (
<div className="itemTotal">
{this.state.itemTotal}
</div>
);
}
Saving the item total in state seems like the right thing to do, because the total is the value that will eventually be rendered out. However, the amount of code required to keep this value in sync with the component's props makes it very difficult to maintain. Additionally, what happens if we need to get an average item total, or a total of a filtered subset of items? Our component logic would quickly become a twisted mess.
Better instead to factor out a helper function that only renders the value when it's needed.
Better
getItemTotal = items =>
items.reduce((sum, thisItem) => sum + thisItem.total, 0);
render() {
return (
<div className="itemTotal">
{this.getItemTotal(this.props.items)}
</div>
);
}
Notice also that our getItemTotal
can act on any array of items and is not tied to this.props
. In the future, it would be easy to arrive at the total of only a subset of items without changing the method.
getItemTotal = items =>
items.reduce((sum, thisItem) => sum + thisItem.total, 0);
render() {
const activeItems = this.props.items.filter(item => item.active);
return (
<div className="activeItemTotal">
{this.getItemTotal(activeItems)}
</div>
);
}
Redux
Redux is a powerful state management library that leverages the Flux architecture pattern. Taking cues from functional programming languages like Haskell and the ML family, Redux allows the creation of deterministic UI components, obfuscating the concept of application state. In this section I'll address three ways to turbocharge your Redux setup.
1) Use Memoized Selectors
Redux's action creators, reducers, and mapStateToProps functions should be pure functions - they should input and output only data with no side-effects. Inherit in this definition is the idea that a pure function will always return the same output given the same input. So when we're building applications with pure functions, and we know that the same input will always yield the same output, why not save the output of a function given a certain input? Why go to the trouble of calculating a return value again if we know it will be the same as the last time we gave the function a certain input? This idea is called memoization, and it is leveraged to great advantage by the npm
package reselect
.
reselect
allows users to create memoized "selectors", functions that calculate a derived value based on input and memoize their output so that the next time the function is called with the same input, it doesn't actually need to calculate anything again. Let's take a look (there are also several good examples of selectors on the reselect
Github page).
Using the above example of getItemTotal
, we know that each time our component receives a new set of props, the item total will be calculated again. The reduce
function has a computational complexity of O(n)
, where n
is the length of the item array, and the filter
function also has a computational complexity of O(n)
, which makes the computational complexity for this component's render method O(n^2)
. However, let's imagine that the items live elsewhere, in a redux store, and that we want to pass just the filtered items into this component. We might do so with a selector.
/// mapStateToProps.js
import { createSelector } from 'reselect';
const itemSelector = state => state.items;
const activeItems = createSelector(itemSelector, items =>
items.filter(item => item.active)
);
const activeItemTotal = createSelector(activeItems, items =>
items.reduce((sum, thisItem) => sum + thisItem.total, 0)
);
export default state => ({ activeItemTotal: activeItemTotal(state) });
Now the computationally complex operation will only happen if the input changes (i.e., the item list has changed). We've also made our component logic more simple- this item subtotal widget is now a one-line arrow function: const SubTotalWidget = ({activeItemTotal}) => <div className="subtotalWidget">activeItemTotal</div>
;
2) Bind Action Creators
Redux's event dispatching system is the heart of its state management functionality. However, it can be tedious to pass down the dispatch function as a prop to every component that needs to dispatch an action. Additionally, it is anathema to the idea of separation of concerns. Ideally only container components would know the mechanism by which actions are dispatched, and the child components would simply dispatch them. Let's refactor two components to accomplish this separation. In this example, Container is a component that is connected to a Redux store, and PurchaseButton is a styled button that handles the actual purchase of an item.
// Container.jsx
...
render() {
return (
<PurchaseButton
itemId={this.props.selectedItem.Id}
dispatch={this.props.dispatch}
/>
);
}
// PurchaseButton.jsx
import { purchaseItem } from '../actions/itemActions';
const PurchaseButton = ({ itemId, dispatch, active }) =>
<button
className={`button ${active ? 'active' : 'inactive'}`}
onClick={() => dispatch(purchaseItem(itemId))}
/>;
It doesn't feel quite right for a button to have so much knowledge about the inner workings of our app. A button has one job: listen for clicks and fire a callback. Let's refactor this setup by binding dispatch to the action creator.
// Container.jsx
import { bindActionCreators } from 'redux';
import { purchaseItem } from '../itemActions';
...
render() {
const clickHandler = bindActionCreators(purchaseItem, this.props.dispatch);
return (
<PurchaseButton
itemId={this.props.selectedItem.id}
handleClick={clickHandler}
/>
);
}
// PurchaseButton.jsx
const PurchaseButton = ({ itemId, clickHandler, active }) =>
<button
className={`button ${active ? 'active' : 'inactive'}`}
onClick={() => clickHandler(itemId)}
/>;
Being intentional with where you import action creators makes it much easier to maintain your code in the long run.
3) Compose Your Reducers
In crafting reducers for your application, you may find that they balloon in size as more action types are added, especially if you are careful to maintain data immutability. However, it is possible to extract multiple reducers within a reducer file to make your code more readable and maintainable. Take this ManualTradeTicket reducer for example:
const selectManualTicket = (state, action) => {
const { tradeActivity } = action;
const { administrator, transactionID } = tradeActivity;
let selectedTradeIds;
if (state.selectedTradeIds[administrator]) {
if (state.selectedTradeIds[administrator].includes(transactionID)) {
selectedTradeIds = {
...state.selectedTradeIds,
[administrator]: state.selectedTradeIds[administrator].filter(
tradeId => tradeId !== transactionID
)
};
} else {
selectedTradeIds = {
...state.selectedTradeIds,
[administrator]: [
...state.selectedTradeIds[administrator],
transactionID
]
};
}
} else {
selectedTradeIds = {
...state.selectedTradeIds,
[administrator]: [transactionID]
};
}
return { ...state, selectedTradeIds };
};
const manualTickets = (state = initialState, action) => {
switch (action.type) {
case SELECT_MANUAL_TICKET:
return selectManualTicket(state, action);
case SELECT_ALL_MANUAL_TICKETS:
return selectAllManualTickets(state, action);
case CLEAR_ALL_SELECTED_MANUAL_TICKETS:
return clearAllSelectedManualTickets(state, action);
default:
return state;
}
};
export default manualTickets;
Extracting the logic for selectManualTicket()
into another reducer makes the reducer itself much easier to read.